#!/usr/bin/env python3.8
# -*- coding: utf-8 -*-

import time, struct, importlib, sys, os
sys.path.insert(0, 'GE')

UL = struct.Struct(">L")
UH = struct.Struct(">H")

"""Searches for stdlib signatures in an N64 ROM, modifying them for use on iQue.
   If that feels understated, you're probably right;*)"""

def search(self, sub, mask=None, pos=0, align=1):
    if align < 1: align = 1
    if mask is None:
            if align == 1:  return self.find(sub, pos)
            while (v := self.find(sub, pos)) >= 0:
                pos = v + 1
                if not (v % align):  break
            return v
    if not mask: return pos	# A mask of 0 collides with anything.
    i = len(sub) - len(mask)
    if i > 0:  mask += b'\xFF' * i
    cmp = bytes(i & j for i,j in zip(sub, mask))
    d = len(sub)
    # Get longest set of valid bits.
    p,j = 0,0
    for i in range(d):
        v = mask[i].bit_length()
        if v > j:
            p,j = i,v
    if (j == 8):	# simple find
        while (v := self.find(sub[p], pos)) >= 0:
            pos = v + 1
            v -= p
            if v < 0:  continue
            if (align > 1) and (v % align):  continue
            for j in range(d):
                if (self[v+j] & mask[j]) != cmp[j]:  break
            else: break
    else:	# Test each possible permutation.
        test = bytearray()
        test.append(cmp[p])
        j = 0x80
        while j:
            if not mask[p] & j:
                for i in range(len(test)): test.append(test[i] | j)
            j >>= 1
        # There's a number of ways to get lowest search, not sure which is best honestly.
        while pos < len(self):
            k = bytearray()	# for safe removal of elements
            v = len(self)
            for i in test:
                j = self.find(i, pos)
                if j < 0: k.append(i)
                else: v = min(v, j)
            for i in k: test.remove(i)
            if not test:  return -1
            pos = v + 1
            v -= p
            if v < 0:  continue
            if (align > 1) and (v % align):  continue
            for j in range(d):
                if (self[v+j] & mask[j]) != cmp[j]:  break
            else: break
    return v

def search_all(self, sub, mask=None, pos=0, align=1, mode='list'):
    """Output modes:\n\t'list'\toffsets only\n\t'+'\tlocations and hex"""
    i = []
    while (v := search(self, sub, mask, pos, align)) >= 0:
        i.append(v)
        pos = v + align
        if pos >= len(self):  break
    if mode and not i:  print("No matches found.")
    elif mode == '+':  print("\n".join([f"0x{j:X}\t{self[j:j+len(sub)].hex(' ', align)}" for j in i]))
    elif mode == 'list':  print(", ".join([f"0x{j:X}" for j in i]))
    return i

def patch(data, src, **kwargs):
    out = bytearray(len(data))
    i = src.p_data if hasattr(src, 'p_data') else data
    out[:len(i)] = i
    for i in getattr(src, 'p_cmds', ()):
        if isinstance(i[-2], str):  v = kwargs[i[-2]].to_bytes(4, 'big')
        elif isinstance(i[-2], int):  v = data[i[-2]:i[-2] + i[-1]]
        else:  v = i[-2]
        if isinstance(i[0], str):
            j = i[0].lower()
            if j == '+':	# Adds to existing value in sample.  Treats source as unsigned, patch as signed!
                # ('+', pos, symbol/value, len)
                k = int.from_bytes(v, 'big')
                k += int.from_bytes(out[i[-3]:i[-3] + i[-1]], 'big', signed=True)
                v = k.to_bytes(i[-1], 'big')
            elif j == 'a':	# Upper/lower halves of address; mostly used w/ symbols
                # ('a', pos (upper 16bits), pos (lower 16 bits), symbol/value, len (when using value))
                q = out[i[-3]-2] >> 2
                if q in (8, 9, 0x18, 0x19):
                    k = int.from_bytes(v, 'big')
                    if k & 0x8000:  k += 0x10000
                    v = k.to_bytes(4, 'big')
                out[i[-4]:i[-4] + 2] = v[0:2]
                out[i[-3]:i[-3] + 2] = v[2:4]
                continue
            elif j in ('j', 'l'):	# converts to jump/jal
                # ('j'/'l', pos, symbol, len (always 4))
                k = int.from_bytes(v, 'big') >> 2
                k &= 0x3FFFFFF
                k |= 0x8000000 if j == 'j' else 0xC000000
                v = k.to_bytes(i[-1], 'big')
            elif j == 'b':	# converts to branch...in theory
                # ('b', source symbol, pos, target symbol, +- offset from symbol)
                v = kwargs[i[-2]] + i[-1]
                k = (kwargs[i[-4]] + i[-3] + 4) & ~3
                q = (v - k) >> 2
                out[i[-3]:i[-3] + 2] = q.to_bytes(2, 'big', signed=True)
                continue
            else:  raise ValueError(f'Unknown option "{i[0]}".')
        out[i[-3]:i[-3] + i[-1]] = v
    return bytes(out)


# For ticket generation and app encryption.  Matches titlekeyIVs to decrypted titlekeys.
# Super-lazy way of encrypting without knowing ECDH keys.
known_titlekeys = {
    b'\xf5m\x02\xce\xc3/-HJ\x89\xc4\x02I\xd3\xbf\xa1': b'\xb5=\xa2\xa1\xf3\x0c\xf2+5\xeb\xdbY^\xf3nt',
    b'ArLX\x8cP\xb5\xb8\x19\xae\xackpG\xaa\xd6': b'\xaa\xc9\xbf\x0c\xac\x89\xab\xdd\xa6\xe35\x9c\xdd{py',
    b'\xb2\xec\xef\x120t,\xb9\x1b\xd2\xd3/\xe41\x0cb': b'#\x1f\xce\xfa\xcd\x85\xfc@\xbb+\x89\xd9\x19U\xe5B',
    b'\xa7k`\x9bN\xdd\x91\xb5\xdf\xa7\xd44\xf34\xc1Q': b'|\xce\x10>a~O\xf9\xcb\x8b\x8a\xd1\xef\x18\x96\x96',
    b',\x8f\xc6\x87\xe0\xfdjKq\n\xd1\xf2\nui\xb9': b'\xb8~J \x84C\xd2\x98\x03\x8e:\xee\x02\x8d\xfb\xd1',
    b'\xb7\xac\xeatN~\x94\x97\xa0|V\xd6\x90\xe1\xa0\x8c': b'\x93\xd9\\r\xce\xd6^\xc0\x1fp\xc4D\x1d\xafY\t',
    b'\xdb\x1b\xd9$\x07\xd0\xc81\x0e\xddaz\x14\xaf\xd95': b'\xa9\xe4\xd2]\xc6`k\xd1\xa6\xb2\x0f\x0c(+/\x04',
    b'\xb6\xb3\xe4m\x9d`\xf3lZ\xa0,\xec/1\xc4\x8d': b'\x81\xa0\xa2\x05x\xd11\x9bG\x07.\x87\xe0<\xf3J',
    b'\x85~\xc9\xebIx\xee\x9a\x94\xe3VY0\x95/A': b'\xd4j\xb8|\x1el2\xb4t\xb4\xec!\x9d\xf7\xb1u',
    b'Bw\x80\x9a*\xfd\t\x15\xcaAS\x817m\xe4\x85': b'\xdb\xd4\xe3\xa0\xfc\xb8\x88\xc3Y\xa2L\x92\x93\r6?',
    b'\x07ij8R\xe9\xc6\xb4\x9a\x1f\xbe\xec\t*\xf1\x9b': b'\xf8\xc8\xfcip\xd5Y\xcc\x08\x0f"Ps\x8a\xcc\xa1',
    b'y\x98![\xf0\xc4\xaf\xeb\xdf\xe5\x17\r\n(\xfa[': b'\x96\xdc\xae==\xb7U\x83QJ\xc3\xf3\x07\x910\x1e',
    b'\xde}s\xdf\x08\x16\x9c7\x13\x9dJP\xee\xf5\x0e\xf4': b'\x9a&\xb17.\x8f.\xa6\x0cYv\xde\xde\x86\x15\n',
    b'\xd0J\x1c\xcc\n\xfdo\r\x0f\xc4;\x88\xa5\x05y*': b'#\x1d4\xec\x10\x19y\\\xd8\x14\x8d|W\xc5\xe6\x8b',
    b'\xc7v#{i\x16\xd8\xedo\x94]\xb76\xaf\xe9\xdb': b'\x11\xf3\xda\xdb\x9b\xad\xf7\x1a\xac\xc8~NY\xa5\xb7\x0f',
    b'$\xcc\xa5\xfa)c\xf2\xbc\x0c1\n\xfbJ/\xbf\x14': b'\x83\x8d\xcf\x07\xa1 u\xefE\xec#\xc3\xbdoWC',
    b'*\xa4.<0h\x06\x0f\xf5\x90\xe3H\xd1|\x14\xf1': b'\x94d\xacs\xfbP\xb7\x1d\xda\xceC"\xe3"gT',
    b'p\xab:G\x8cf\xc545J\x11\xbe\xf9EH\x18': b'\xea\x04\xfbjqAb\xb7\xe3\x97f\r\x8fI\x0b\xf5',
    b'\xf6\x02\x04\xdd\x11\xf2\x88J\xa4I\xc7\x10\x8e\x1cJ2': b'\x975-~\t\xac\x96\xff\x84\xcc\x93\x1d\t\xf4(\n',
    b'\xa3U\xe7C\xbf\xde-x\xd9Lw\x9a\xc9=5k': b'\xd5qG\xfd-<\xca\x83i\xa9f\xac\x9f\xec\xbf"',
    b'\xc9\x97\xd7\x93\x94\x05\xf4\xd9\xaat\x0c\xafD\xfe\xf7\xf4': b'\x16%\x98\xca\xc3\xcb,\x01\xc2\xff\t\x04f\x9df\x92',
    b'\xfa\xa4\x968/\x83\xc2\xee\xcd\x8b\x80\xca\x05\xcfr\xc6': b'\x12\x18\x19\xe3\xba[\xa2\xa9\xa6\x85t%\xca9RJ',
    b'\xea\x1eo\xd5\xd4oj\xc5\x89\xc0\x00 uf\x01\x95': b'$\xe7\x94\x08\x9a;\xe2\x98 \xd9L\xdb\x93\t\xb9?',
    b'A\x13w0H^\xefo\xff_\xd5\xee\xcc/\x91\xc9': b'`\x8b+\xces\xf5\xfan\xa4B\x1fb\x1e:\xb2\x1a',
    b'v\xd3\x96\x12R\x81\x95\x9f8\x9a\xeaN\xbe\xdf\x82\xbf': b'\x02\x00\x16\xf7\xb8\x99.\xeb<\x8c\x8a\n\xb8\x01\x02f',
    b'\xd2~\xe7\xea\x84\xf8\x03 j\xaeF|\x19\x86+\xf0': b'p\xcbO\xc0\xec.\x1f\xa4\xee\xc5\xdb\x83\x87\xd3>\x87',
    b'\xbc*y\x9fZ\xf1c\x93\xa0\x08\xd9\x14\x7f\x86\xab3': b'L\xb5x\x802\x1b:\x85D\xd0\t\xb0\xd5]\xdd\x83',
    b"\x94\xfa@K\xdf\xb0'2\x87\x08b\xe4\x9e\xa2\t/": b'\x07\xf2\x19+\x96\xe6\xaa}\x86\x0f\x0f\xee\x10\xe3\xc6\xde',
    b')@\xae\xf9\xe3\x8e\xe3\x8e0$\xc4\xab\xf0bt\xe8': b'\xc6\\\xc4\xeb\xbb\x19a\xb4\t\x80K\xd5h\x8d \x10',
    b'C\x95\x89\xb1j\xda\xecW\xbf\xa9!\xcbGG\xa4A': b'$3\xebXV\xb1\x8c\xc4\x96n\x0e\x0c\xf6\xbe_i',
    b'\xae\xb0\xb0JL\xe8;\n\x82 =\xd2\xfc;4}': b'\xdbR\xcdR`u\x93nX\xd5E\xa3[w\xfa\xaf',
    b'\x10\x90\xc5\x93-\x1d\xcb\xc6\xe6\x90\xab\xa6\xd1\xaf@c': b'$dE\\\xbc\xca\xce\xe9_\x95\xa9\x9a\x9e\xaf\xc1\t',
    b"\x81!\xd5\x8d\x0bf\xd3\x13%\xbchke'\x93\xc1": b'\xf1\xdb\xd7 \x1f\xe0\xe1\xeb>\xd27\x04j\xe8\xc3\xa7',
    b'V\x82!\xeewa-\x986\xe9+u\xa6\x96.\x7f': b'Zb\x1f\xc4\x99VFP\xca.\x9ce\x1c\xea\xd4R',
    b'\xb6\xd2\x82\xc1S\xf4\xaa\xe9\xe1\x17\x97\xde\x03\xfd\xee\xd3': b'v S\xe3?\x11C8\x19\x02\xff\xf2\xee!J\xf9',
    b',\x18(\x9c\xeein\xbd\xfd\xa7~\xbb\x80\x93\xbc\\': b'LF\xe5\xe5\xceW}&\x9e\xc6*\xac\x16\t\xfeN',
    bytes(16): bytes(16),
}

if importlib.util.find_spec('Crypto.Cipher'):
    from Crypto.Cipher import AES
    def encrypt(src, iv, key):
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return cipher.encrypt(src)
elif importlib.util.find_spec('aes'):
    import slowaes
    def encrypt(src, iv, key):
        cipher = aes.AESModeOfOperationCBC(key, iv)
        return b''.join(cipher.encrypt(src[i:i+16]) for i in range(0, len(src), 16))
else:
    print("AES encryption not available.  (ノ ゜Д゜)ノ ︵ ┻━┻")
    encrypt = None

def encodeApp(cmd, src):
    if encrypt is None:  return
    titlekey = known_titlekeys.get(cmd[0x2814:0x2824], None)
    if titlekey is None:  print("Not a recognized app, you'll have to do this the hard way.")
    else:
        print(f'Encrypting with:\n\tTK\t{titlekey.hex().upper()}\n\tIV\t{cmd[0x2838:0x2848].hex().upper()}')
        o = encrypt(src, cmd[0x2838:0x2848], titlekey)
        return o

aegh = b'\t\x93\x07\x67\x85XMh[W\x16N(\xc6\x16"\xd4\xa2\x14lJ\to\x11\x8a\x96ZjY\x19a$aB$B\x88D(\xb163\xbc\xd5\xa0M\xb16\x9d\x91\x84+\xff`\xd2H\x8b\xc4\x12%\x99y\xd0\x99\xf6m\xec\xbcEi\xb50\xae$T\xd7\x12^\xf4\x89\x0e\xb1M\xa7\xa9Ea\xa6\x82\x16W\x9b\xda\xaa\xbe~\x1c\xee\x95\xe32\\,=?\xbdw\xbe{\xce\xf9\xce\xdf\x9di\xce\x8c\xd6ta\xba0\xd3\xe42<\xf2mx\xfc1Y\x81u\xc3357]\xf0\xc7\x0c\x0f\xae\xf1-w\xf9\\\xb0\xa3\xe4\xe0.\x9e\xe0/\x81u\x91=\xd3\x84\x0c\xf5\x1f\x9e\xd0\x17\x9f\x96\x85\xa7\x81\x86{j\'Ss3M\xe2A\x12\xfe\xb0\xe3+W\xfd1\xb5g\x85\xa0kG\x0c\xf5\x14\xbf\x05-\xd8\t\xacO\x17\xb0o\xe2\t&\xdeVZ\x10\x8f\xcf\xd1\n\xb0\x01\xe5\x8a~D\xe3\xb7\xae\x99n\x11"\x06;x\x1eH\x90\x83E\r\xc5\xe2\x94J\xcb\xf3)jGI\xc4\xe3\x124~\x8a\xcf\x94\xf7tD\xfe\x0f\x0b\xf2\xee\xd4\x1c\xf5\xa5D\xb1\xb1\xec\x86X\x90)\x9a\xab\x05T\xeeDa\xf0\nH\\\x90FI\xc4\xc3\x0e\xf8$\xe5r\x07\xc2\'\xb9+\xfb\x16\xb6\x00C\xe1\x02Oa\xe2\x1b\xb2\x05\x11\xbb\x12\xfd\xf8?\xaf\xa9\t\xae\xc0\x16\xe2)\xed\xa8\xe98s\x88K\x9d\xa1)\x91\xc4n:"\xf1\xf8\x9b\xa0\x88\xe5\x19\rbu\x9d\xe9\nO\xe7\x90\xae-\xf6N\xed\xc6\x11\xa7\xe6B\xbe`\'\x9a\x88&B\xbe\x90\xcf\x1f\x0b\xf9Dc\xf8H\xbd%,\x95\x88\xd5#\xfdb\xfc\x91\xdd\xd44\xd8\x81T\xc8\x00N\xc8\x87wC\xbe\xf4l\xb2\x9b\x9e\x8d\x97\x03\xeb\xf8\x15xd\x8bd\x00\xe1\x0c\xa3\x96<\x1a\xb7\xe6\xcb\xf1.,6<@\x12\x1d\xa3\x89\x99fz6S\xc52S!_4A\xfd\xf0G\xfe\xf0ZlJ[\xaa\xe8\xd3\xf3\x97\xc4\xfcx~\x81\xb6\xb4\x11\x90\xa3\th\x967\xd6\xd2Xf\x8ax\xc4\x04[\xe8Y\xdd\xab`\xede\xb6|9\xe2\xe4*\xe4\xc3\xee\xe8\xbfd\xd7L\xc5\xcb\x95\xdeZ:o\xe4\r3\x95\xec\xd2\xde\x8c\x0f\x15C\x12\xa9\xb0%\xfc\xf2\xb2\x15\x15\x9ep\x83W\xd0\tk\xba\x10M\xc4\xcbf\xcaL\xe5\x8d\xec14\xcc\x1e\xc3\x8f\x82\x07\x99\xeam\xc9\x84\xcc$\xc8\xe9:\x9e^\x0fT\x84\x83\x81@\x8b\x97\xe3\xe5h\xc2L\xf9c\xf1r\xa6\x9a=\x06N\xc9*Y\xf8N\xcf\x82\xa1\xf00\xac.;$"~\x81v\xb8\xbe\x8c-\n\x8d8\xe4\t8\x02\x9f\x01\t\x9f%\x8b\xde\xb3\xfc\x96\x1f\x1c\x85T<;5\'X\xc4c\x9e\xe7.$\xa7\xab\xfa\xa0{\x8ez\tr\xbc\x0c\x9f\x01\xa3\xd2\xb3\xfc\xd0\xcb\xf2Wz\x99\xaamZ\xfe\xb54\xf5#\xde\xf8{\xb03<G\x1d.\xab\xad\\xV\xa2\x87\x11@\xc4\xecq\xad\xdf\x08\xb43%\xab\xd6\xaf\xf5\xa9\x1f\x18\n\r\xa3\x89`\x87\xec\xc5\x95\xe8x\xe5*\x99\r\xd6\x08\x9e\xa0\xd0\xa2\xcc\xdb\x88\xe8d7\xd9\xc5\xae\xe1\xb7J\x8fH\x8e]\xb2\x1c\xbb\x9d\xa9\xf4\xda\x99F\xc0\xf2\xdbf\xc92S\xd0\x10\xfb\x17f\xd1\x17`+\xee1_\xe0w\x95Mh[\xc6$\xbc\x0c\x8d\x04+o\x94,\xdbtl\xd8\x12\xdf\xb6\xd9\xce\xb8N;s4\xe0]x\x14\x1a\x82\xbd\xf2\x1e\xde\x84F\xf4\r5\x94\xac%\x95\tW\xca\xd3`H\xb2\x9b\xa9"\xca\xc0\xc3\x92U\xe9\x01\xb1\xd6\x07\x1a\xf0\\\xc7\xb1k}\xcb\x9f\xa9\xe6\r\xb0f\xa6)<\xa6\x8e\xcc\xa4\xc8\x06\x94\t\xbbI=`]!\xa3\xc8Q\xc9"\xd9\xe3d\x17\x16tlp\xc46\x8f\x06\xbd`?{48\x1a\xb8N#\x00LF\xbdXE\xf0\x90\x05!\x05\xf7\x05oj\x0e\xfed\x8c\xea\x0b\xde\xc3\xbby\x03{\x07\x9a\xeb\xd8&\xb4k\x04\xfa\xd9AmP\xebg{Ax\xb0\x9d\xa9\xf5\xa1}\xa6:\xd3\x84=\x92]\xe0\xdd\xef\xbc;\xf2\x0f}H\xc6\x02Q\xd59\xc9\xe5r\x85}\x85|\xc8#\xc4\x80T\xd8\xd0uzA\xa0\x11\xd1u\xc0!Z\x94x\xf1\xf2\xfd\xce\x8ew\xc7\xfbQ\xebS\xef\xfbM\xa2\xd1\x9b\xe4\x93b\xa6TH\xfa\x17O\x02\x13^\xb3M\xc8\x84\x8e\xd4L\xc7k\x04\x1a\x01\xd8\x18|Iv\x17\xcb-\xef\xc1\xc2\x9f\n\xe8\x08?j\xbd\xbb\xcc\x1a)|e\xbd\n\xee[F\xfb\xad\xfa\xf7\xed\xb7*U\xc9D\xe0\x13\xb2A4\x91\xa9B\xbfJ\x0f\xbekgt4,Z\x149`\xb4\xafT\xfd\xbb\xff~xh4n4n\x04\x8ao{\x0eb:{\x04\xafq}\xf8O\xf7\xef\xf1Q\x86[{\xfd\xe8\xc9h?7\xeb\x87\xf5C\'\xd5\xben\xbe\x9a}p\xf2\x9d\xfbN\xfez\xb2[\xdb\xec\x19\xed7\xddw\xdc{\xd6\x8d\xca\xa3\xbe\xe1\xdek\xbfy\xf4a\xe5\xd1\xc9\xf1\xe8\xf3\xfbQt\xbc\xe1\xde\xdb\xd9\x1f>qS\x8b\xfb\x8b\xfb\xce\x9d\x7f<\xfc\xe5\x8d\xcf_\x0b\xf9\xb2\x0f\x8e6K\x89\x93C\xf7n{\xd6\xbd[\xbaU\xd9\x18>:\xe9\xb2o\xael8\xbe\xca\xc6\xe0\xceI\x19|--\x0c\xef\xe4\xf6\x86w\x9cWaS\xe7\xe6\xf06r\x9asmx\xbb\xb2>,\xaf]K\xbf\x9e]7o\x0f^;y0H\x94\x12\xfb\x9d\xd3\x83o\xd7\x87\x89\x1f\x0f\xf6\xf7O6\xa2\xe1\x1d\xef\x8b\x8d\x81o\xcds\xb2\x91\x1d\xb1fx\xf3\x93\x96?\x9c\t\x87&s\xf5\xa1\xf7\xc7Uz\xce\x99\x1cz\xf9\x87\x0ckL\x0c\xbd\xf5\xf9\xf3\xbdg#\xe6\xe6\x8dg\xcd\xf3\xbdF\xa0\x17Lv\xcf\xf7\x0e[\xe7{_/\xd7\xe7?Yv\xec_"\x8b\x1b\xa3\xdf\x8a\xa7\xad_\xf7\xee\x86\xcfW\xf7\xe7\rO.\xbc\x13;\xf0N\x17\xbeh\x9eG\x16Gl\x8c&\xce#\x87\xc5/\x9a\xcf\xc2\xa8\x13\x9b\x85\xd3\xe2\xff\xe6?\xae?\x0c?o\x9e\xb5V_9\x9c<-.\x15n5\x9f7O\x8b\xc9\xb9\xb3\xe2W\xf3\xab\x9e\xed\xfa\xe7\x13g\xc5\x87\xb76\xd7_4O#K\xad\xb3\xe2v}\xa5\xf9qa\xf3\x9b\xd3\xc9\xdc\xa3h\xe2\xe9\xfc\xaf\xad\x17\xcb\xc9kg\x91\xe7\xcb+\xb9\xffx\x7fnE\xc2\xd1\xc4v\xe1ld\xb5\x91\xff\x1e\x9ey>+,\xbe\xf2xw\xa5\x8e\xdemj\xeen\xee\xf1\xee\xf6\xc4\xe3\xdd\xc8\xef=\xd9\xfd\xbfnO|\x16^\x19E\xca_\xea\xdb\x13\\Ow\xf1\xb9U_\x8a$\xbb+\x11\\a\xfdmu\xab\xbe\xb2\xea:\xdf.<\xf1<\xcd}\x10\xde\xda\xfdr\xf9\xa7\xd8\xbf\x8b[\xbb\xd1Q\xe6\xfdWnkw\xc9\x13//Mn\xedr\xfd9\'=\x11\xf3&\xfb\\\xe6\x1a\xd4GVe\xe68p,\xd9\xcd\x1e\x0fj\x99*\xea+3\x1d*\x86c\xff\xb0\x00\x7f\xdc\xf7\x1cx\xbf\x19\xc5\x02\xb21b\x83}z\xbc\xcc\xdc\x0c\x89\xe0\x89\x8av\xc9\xdb\xaa\x8b\x08\xf9\x10\xc1\xe0\x0b"\xd0u\x8e\x06\xe3\xf1\xc7\x88\xb0\xcd\x1f\x16>\xf5\x1e\x8c\xd6{\xcb\xd8\x19v\xc3\xfd \x16\xb0\x18\xe9W\xaeJ\xa7\xa8\xa3\xa99\x041hx\xf0\x1ej\xab\xe4\xe8\x8b\x11\xdf\xcf6\x02Q\xdf\xdb1ff\xe4#\xa2\x11I\xb2\xb6\xe41}\xf2\x82\xce\xaa\xbeK\xcc\xc3b\xc4CN\x03\xa6\xae%\xb2\xb6\xeb \xa3#\x7fR3\xa2\xb1\x92I\x17(UGM\x962\x89I\x16\x05\x1a\xf2-\xf0X\x83\xa0!>\x1b\x01\xd6\x07\xe4k\xd4\xa4v\xc66YCPM\x94%)S21?\xa5\xbb\xe7\xcc\xa0w\x13\xac\xfe\x88\xf1\xf4,\xf2#\xea\x1c\xa4\xb3R\x10\x0f\xda\xa1\xc2\xaf\xa5\xa1\x1b\xab%\x16;U\xb2\x8cuW\x96\x9aI\xe5,`\xbc\xcf\xc5\x1b\xe9\xd9\xec\xb1\xe5wldf\xd7\xb1\xfc\xf0!\x17\xf4Cw!^K\xcf\xb2#\xf2\xc7X\xe3\xe9A\xa9FX\xaa\x8f\xb889K\x9f\x84\x885S\xc0C\xc6\x06\xa6m\xc2\x7f@\xab\xf5\xd1a\xb3\x9b\xe7\xbc\xc8\xb8Q\\Q\xb5\x97\xb8\x12\x19zE\x12\xed\x18\x19\xd8k\xb2\x8b\nP\xe9\xe5\rX\x16\xb1A[\x8av\x8c\xcd\x8bK\xa2Au3\xc0cW\xaf\xa3\xc9"\xa7\xd0\xdf\xb1\x87\x01\x1e:\\\xd4&p\x06LA\'\xc1\x19I,J\xbe\x8c#*/\xb2\x7f\x11\xed\xf4\xfa\xce\x1a\x0f<\xf4;\xe8\xe09)P\xc7Z\x1f\x0crl0\x05h\x82\xa5\xf8\xa9G\xa0Z\xe29\xbd\x07\x15\xedhY\xe2a\xb6d\'\x8f\x19\xa5\xd2+Y\xe8\xa9\x11+\x9c9\xa9\x8f\x1e\x7f\xe3X\xd2\x932\xc6\xc7\xe7=\xc9\x00\xcci\xe8\xa4\x18=\x9c/\xd1a3\xba\xcd\x14\xfc\x8bx\xb9\xdcs:\x9a\xf8o|&Sxr\x06@{\xca\xe4\x02DX\x14\xdd;\x16$#/\xe8l\xd4Y\xa2\xb0d\xb1"\xe8]\xbd:\xab\x12{\xaa\x0e\x8a~\x047\xf0.e\xb2\xdf\xc7\xb3\x94\xcf\xbb:O\xf4\xf9\x85\xb6\xbclfQ\xb3\xa2`\x8bU\x90?\xe2e\x95;(\x99\xbd\x17\'\x0e\xd5!]\xc4\xbb8\xfd\xa9\xf3\x0bu6\xa0\xf0\x88\x01\xd9\nE2\x89\xcc\x89\x8c.\x9d%\xd2\x9b\x11O\x9dwJL\xb06\xc9\x99\x12O:\x94v\xf2\xb6\xa0\xc8|\xa2\xa6\x14\xbcA\xde\xcb$"h\x9cW.\xe2Q?\x1dOYSv\x8c;\xe0\x8a\xec@\xcd\xf7\xa2\x15}\xc5Y\x96\x91\xccx\x90\xa5b~\xfc\x1cD\xceQ\xd4|\xad\xec\xa3[LqJzs\xe9p9O\xf2\x8cR\x9d\x91\xaa\x93.9\xe5\x13\r%\x9f\xca\xc4*\xfb\x1f\xc7\x1b\xd7\x92X\xcc\xdf\x82\xaa\x9f[\x8f\x9fS"\xc6\xd5)\x85>\xd1\xab\xf3\n\x89\x01\x89d\xf8O\xedG\xd9\x1eXb\x1d\x9e\x1c\xfc?<\xb1-\xa5\x81\xf3j\x9e!\xa2\xe0\xe9\xb9\x84\xde\x15t9GU\xfa\xa9(\xfc#<\xe6\x1b~\xcat\xceS\x89\x8b\x99K"N\xf771\xf5Sk=\x12\xf5S&\xfd\xfcX\x97$vS\x16\xd6#[?/\x12\xee\x04;\xbf\x01p)\'\xc6\x80\x18\x00\x00\xedY}L\x95U\x18\x7f7\xf9C\x97k\x8e\xa2\x98\x1fP\x18\x08\x98\x98J\xa2^\x90\xa1\t%N\x916,\xa6s6\xc6\xc6j6l+\x9b\xab\xe6\x1asRfevss&\xce\x89\xa69\xa9\x8c2D\xdb\xc4B,C\xbe\x15\xe4K\x14\xbf\x01?\xe0r\xef}\x9f~\xe79\xe7\xbd\xf7\xbd(\x1fmx\xa3\xc6\xfb\xfcq\xcf{\xces\xcey\x9e\xdf\xf3q\x9e\xf7\\M\xfb\xff<\xc9#\x93"_\xfabn\xd5\xf37#\xee\xcd\xbc\x96\x14\xa7\r?\x0f\xedY\x1a\x96\xb0gF\xfb\xa4\xee\xc8[\x0b\xf2\x17\xbd\xb3h\xcd\xcc\xeb\xb3/%\xfb\x0c\xe3\xf20\x9e%\t\xb1e\xcf8&u\xcf/J\x8a1\xfa\x12\xdf\x9f\xde\x91<r\x18\x9b\xc1~\x92b\xa2\xeb\x02\x9c!\xf6\xf8\xef\x96\x06\x99\xfb\x13\xf6G]\x1d\xe2\xfe\xed\xab-\xd3\xd2z\xa1e\x18\x1drRN\\\x13[\x17\xa8O\xd0c\xeb\xc2\xdf\xed)\xe7\xf4\x8e\x05\xf9C\x1ci\xabf\xd7\xf4^\xc8\xaem\x1a\x12\x88\x8f\x80\xa46!\x91\x8f\x1e\xa9\x87\xd1\x134\x8d&?@\xce\xc4\rA\x8e\xa49C\x16\xedM\ni\x9aGY\xb4\x97\x8e\xd1\x1fTNg\xe8\x04\x1d$+\xa5\x93?i\x04\x8ee\x9a_\x1f\x11\xd03\x1e\x82\x06\xcc\xdb\xf7:\xbe\x9eq7\xfe\x9b(:\x04\xb9*\xa8\x90\x8eR#\xb7\rZ)\xe4th\xd6\xf0\xd7\'wY\x1a\x91M|\x07$\x83wcW\xa0\xed\xd0\x80\xe9\x1e\xbaD\xdd\xa4\x93\x83:\xa8\x81\xca\xa8\x8an\xe2\x8d\xc8F\xef\x91\x85`\x8d\x9c>#\xa0g<\x14\x0e\x98\xb7\xefu\xac\x9e\xbb\x8e\xd1\xe7@J\xf1\xe8\x90\xccMB\xd2|!\'\xf9\xe8\xb3\xf5@}\xf6\x16\xc6\xda6\xa0=\xbc\x19\xbbV\x81\xf6f`,$v\xd0\x9f\xb4\x16R\x1b\xb4\x95n\x90\x936\xb0\x1e2\x02\x96\xd3a\xaa\x84_5S-\x15\xd1G\xd2\xf7\x11\x17\xbfQ\x1dS-\x9d\xa5\xd5\xdc\xa7\xd1\xdb\xe0\xac5Q\rV?H\xaf\xf0\x98?\x1d\xf0\x183h\x17e\xa8Y\xd5\xb4\xce\xb5\xeb\x07x\x93+L\x07u1\xde]\xf4&\xa5*ZA\x1b!\xd3\xf7T\x02\x9e&z\x94\xc2(X\xe0X\xa4\xe9\xab!O-\x9d\xa7\x02X\xa3\x1e~\xd4\x93\xacF\xecz\xeb\x816\x87\x81\xa9\xd4 [\xe1\xac\xb9h!\xf2\xcb\n\xee\xf1\xa7Mt\x05\x9c\xdd\x88\xe5=\xb4\x83~A<8\xe9\x1e~#0\xba\x8e:y\x8dk\xf4\x19\xc5\xab\x15\x82\x81\xa9X\xd9\t\xeb4\xd3E\xcc\xbe\xcb\xf1\xd3\xccH\xae\xc2|\xf1t\xf0\xa8 \xb1z9\xc5!O\xe8<b\x87u\xa4=#\xe8g\xf4\xe9TJ/P\x80\x92\xb6\x138[<(\x1a\x9e\x91K\x8f\xd3\x14\xe4\xc3`%\x7f\x04\x9d\xc3\xbc\x06\xd8f\x05]\xe7\xa8h\x85\x05$\x9d\x87.E2v\xd3\xbc\x867\xbd\x05\xbdd|\xee0\xb0f\x9f2\xc8\xb0\xc0q\xd6\xf3\x8e\xc9\xff\xe3\xd8R:\x90L\xc1x\x1b\xaf\xf1\xb1\x87\xc52\x81.A+cF<\xf0\xb0q\x1c\x15`\xb4\x94g\x14\x9b\x10[\x8f\x88\xb2\x00\x1b\x1d2\xd9\x18\xe1*\xa0*V\xca\xe0\xb7\n\x1aGO\xc2\xc6\xa2\xdd\t\x04\xe3`\x0f\x7fhPN3\x99+\x9cf\xd1S\x18s\x02\xc7xF\xfb,\xf6:\t>\xb1z\x03\xcf+4\xed\x97\x06\t\xbd\x8d\xf71\xd6Z\x87\x16\n\'\x9c8\xc8\xd5\xfa<HV\xa3\xa8\x02\xf2;\x99\xebK\x0f\x9bX\xe0\x93\xa2\xf7/\xb4nq+\xdb\x18\xe5hN\x87\xb6"\xcf\x9ag\xe4\xf2JN\xda\x89\x13Y\xcc\xf8]Yg-mS\xd6}\r\xbd\xb7\x11\x037y\xfc\x16}H"\x8f\x89\xf6>T$s\x14nN\xf0t0\xd9\xb1K\n\xe6M\xa2P\xa0\x7f\x086\x12;\xd4\x00\xcd:\x8c\xe5\xbb<\xa0\x9e\xe7\x15\xa8\xfd>\xe5\\b\xf1:\xdeGY\n\x897\xa3b\xe5:\xc4.r\xc4U\xa5\xd9qD\xbb\xe1S.\x9bp\xe6?\xa5\xfa=\xf0\x16g\xeb\xabZ\xa6\xc0\xdbn\xe0-\xcf[\x87\xd0\xb4\x939[\x15\xde\x9d\x88\x8c6j\x07g\xbdBB\xe2m\x01\x86\x95\x8c\x9c\x8d\xf2\x14\xde\x07\xe1\xa9>\no\x1b\xderA{\xe1\rw)\x06#\xe1\xe4\x07khh\x9d`;w\x81\'\xd7\x14m\rJO\xb1_\x07V.34\xb6y1\x7f\xe3t\xebV\xa8n\x91\xbb\xe7p5\xc7UK)\x8f8\x10\xe1\x8d\n\x9bT\x89\x9d\xb0I0j\x10\x85\xb7\xad\'\xde\x99\xc0;\xc7\xc3\xbfE_:|\x1e{\xc8|p\x03x\x8b\xc8\xaa\xc4\x9cl\xe4\xa1\n\xec\xe6\xc6\xfb\x0e\xb7\xe3\xe0\xadv\xe6\x92\xfbW#Gkh\xf7\xcc\xdf\x198\xb1\xc3\x18\xedQ*\x07^\xe0Yy\xee\x98E\xb45r\xdfi\xd4\xbcY\xc8\xf4g8\xb3`\xac\x08\xde\xed\xeb\xcd\xf3\xf2G\x95+n#\x9b\xa9\\`\x05:\xc0\xe6,k\xe6@\xffn>\xc3\x9c\x9c\x9f\xd9&i\xd2[\xaf0G\x13ze\xfeVx\x17q\rG\xe9\xae\xfc\xed\xee\xdb\xcc\xe7\x80\x0e$Ky\xee)\x17j\x0bMx\xdfUm\x91\xd3\xdb\x89\xd4\ts\x80{\x9b\xd4\xe9\x9e\xea\xf2\xdc\x8d8\x1f\xa7"\xab\x05\xbbN\x9d\x1a\xe6\xff\xda@{\x9b\xb0\xbe\xb4\xd31\xd3\xf9\xa3F\xbd^\x0fn\x07\xd6:\xebpD~5\xc8\xca\x14>G\x8cr&d;\xcd\x1cm\xf0%\xf7x\x1e\xf7\xdd\xa17 \xb9D%\xdb\x85\xc1Jdd\x13\xde\xaa\n\xfc\x1c\xde-V<\x8d\xbe2\x9eQb\xaa\x87\xbe\x82$\x02or\xe1-\xe8e\x17\xe2\x12\xeff\x85w\x8a\xe2\xd9O\x81\x14\x84]\xf6\x9ar\xc79\xc6{\xa7\xe1\xc19BK9\xef\xb8i\xe5\x9d8\x8f\xbc\\\x0f\xaa\xef\x9d\x85\xd0\xa5\x953\x8b\x1d\x99\xa1\x015k\x0b\xb0u\xb0w\xb7\xe1\xe4\x11\x9a\xec\xe3*\xdd\x06\xaf\xcf\x85\xa4\xf9t\x99\xb9ky4KU\xc5W\xa0U\x13\xe8\x12F\x1a\xc0\xa3\xb3\xde\xb2\xaf\x85\xad\xeaD&\xd9\xceU\xc85\x9eq[\x8d6\xe1]d\x8c\x14d_a\xa3,\x89\xc9\x05\xcd\x19Ic\xf1.\xa2\xe2[\xcc\x9b\x06\xfbJ/h\xe1\x99/\xd2hz\xda\xf4\xbd#\xac\xba\x91\xa3\x8d`\xd1T\xd7I\xbd\x12z\x89\xe72l]\x02:\xc5\xf9\xbc\xd8\xdb\xe7\xa5\x0bq\x81\xe8*\xd8\xfb\'d\xb6\n\x9c\xf1U\x88\xf7BD\xe4ZS\x85\x17\x8f\x1c_\x02\x89o\xa0\x96mE\xcc\xe6\x0151\xb6\x04Ux\xd5}t\x04u\xb0gO)\xfd\x80\xfc c9\x17\x99[\xf6V+\x12\xed]\xf0\xf12n\x1d\xc5\xaa"\xda\xc7\xfc:\x155\xf5a\xfa\x04\x969@\xb1\xc8\xd1\xeey\x17\x91\xb3\x1fA\xd5W\xc3}\xea\xbb\x0c\xc8\x9eT\xab\x95s=%\xbf\x05\xf6\x83\xa7\xfa>\xda\xea}\xbc=nP,\x0f$w\x06\xe9e\xb4\x97y\x96\xde\xd7\x1b(\xa7\x1e[\xe0G\xcf\xd1\x18\xb4\xe7\x82\xdcs\xa3qF\x06\xa2\x1a\x9fE\x13z\xac\xac=`7\xad\xcf=\xbc\x8cw\xff7\x84\xe2\x04M\xfb\x07\xf7\'\x83G\xb6\xa86?\x8a)\xf6\xdcy\x94>C\x9f\x02\xdf\x0e\xa1(\xf2\xe9\xff\xe6\xa6\xbf\xdb\x1co\xd6\x83\x03\xbb\x01\x17wh#\x06x\xdf6\xa8\x14\x9a\x11\xda\x1db\x8f\x9bl\xde9z{T\xbb?\x05\xe81-\xae{\xef\xbeo&\xfb\xbb\xad\x1c:\xb7\xfb\xff\xfe\xff\x92!\x13\x1d\x11\xf7\xdc\xff\x08\'\xae\x8f\xba:N\x0ftF\xd7/I\x18F\xe7!\xfc\xe7>zZG\x803q\xfd\xd2\xb0\xc4\rs+C\xbb\xc7\xea\xe1]\xf3O\xb8\xff\xa7\x1c~\x06\xfbY\xbc\xfc\xd9\xce\xf1z\x90C\xfc\xf7>\xafx\xf1\xf2\xe4\xc7\xfe\xcb\xda\xfc\r\xbf\x16l\x1b\x80"\x00\x00\xa3\xb0\xa3\xb0\xa3\xb7\xa1\xa1\xa3\xc7\xa3\xef\xa3\xec\xa3\xe4\xa3\xe5\xa3\xee\xa3\xc5\xa3\xf9\xa3\xe5\x00'


# Thing to do N64 cart stuff.
class Cart():
    cic_names = {# I prefer the old 'scene names, they're fun.
        "starf":0x3F,
        "lylat":0x3F,
        "mario":0x3F,
        "diddy":0x78,
        "aleck":0xAC,
        "zelda":0x91,
        "yoshi":0x85,
        "ddipl":0xDD,
        "dddev":0xDD,
        "ddusa":0xDE,
        "64sdk":0,   # Doesn't run a checksum in IPL3
        }

    def __init__(self, rom: (bytes, bytearray)):
        """Sets ROM data, unswaps it if swapped,
            and extracts some handy header data.

        Expects rom to be a bytes or bytearray object."""
        from collections import namedtuple
        self.rom = bytearray(rom)
        self.straighten()

    def __str__(self):
        """Returns the ROM's internal name, followed by gameID code and version number if applicable."""
        i = self.ID
        return "{}-{}{}{}{}".format(i.title, i.frmt, i.name, i.region, '' if not i.version else str(i.version))

    @property
    def ID(self):
        """Extracts header data from an unbyteswapped (presumably) N64 ROM.
        IMPORTANT NOTE: currently skipping initialization fields. (ie. PI settings, boot address, clock speed, etc.)

        Returns a namedtuple with the following fields:
            title   cp932/936 string with encoded filename
            frmt    'N' for carts, 'D' for disk, 'C'/'E' for cart/expansion titles; 'B' for iQue multiplatform conversions
            name    2-char name; (mostly) unique for each title
            region  char designating intended region
            version integer value, starting at 0, for each release version
        """
        from collections import namedtuple
        # get ID
        frmt=self.rom[59:60].decode(errors="replace")
        ID  =self.rom[60:62].decode(errors="replace")
        reg =self.rom[62:63].decode(errors="replace")
        ver =self.rom[63]
        title = self.rom[32:52].decode(encoding='cp936' if frmt=='B' else 'cp932',errors='replace').strip()
        clck=int.from_bytes(self.rom[4:8],  byteorder='big') & ~0xF
        pc  =int.from_bytes(self.rom[8:12], byteorder='big')
        rls =int.from_bytes(self.rom[12:16],byteorder='big')
        nt  =namedtuple('ID', ['initPI', 'clock', 'PC', 'release', 'title', 'frmt', 'name', 'region', 'version'])
        pi  =namedtuple('PI', ['release', 'pagesize', 'pulsewidth', 'latency'])
        p = pi(self.rom[1]>>4, self.rom[1]&0xF, self.rom[2], self.rom[3])
        return nt(p, clck, pc, rls, title, frmt, ID, reg, ver)

    @property
    def cic(self):
        """Guesses the CIC from the ROM.
        This is a weak test designed to allow minor hacks.
        If a ROM or bootstrap isn't present, will return None"""
        if len(self.rom) < 0x1000:
            return None
        cic = {
            0:"starf",
            0xD5:"lylat",
            0xDE:"mario",
            0xDB:"diddy",
            0xE4:"aleck",
            0x14:"zelda",
            0xEC:"yoshi",
            }.get(self.rom[0x173], 'mario')
        if cic == "starf":
            if self.rom[0x16F]==0xDB:
                cic = "ddipl" if self.rom[0x5F3] == 0x21 else "ddusa"
            elif self.rom[0x16F]==0xD9:
                cic = "dddev"
        elif cic == 'mario':
            if self.rom[0x598]==0:
                cic = "64sdk"
        return cic

    @property
    def entrypoint(self, cic=None):
        """Returns the actual entrypoint, after any necessary correction.
        If cic is None, uses the CIC derived from the ROM.
            Otherwise acts as an override."""
        if cic is None:
            cic = self.cic
        i = self.ID.pc
        if cic in ("diddy", "aleck",):
            i -= 0x100000
        if cic  == "yoshi":
            i -= 0x200000
        if cic == 'lylat':
            # Fixed entrypoint!
            i = 0x80000480
        return i

    @property
    def gameID(self):
        """Returns the gameID as a string: frmt, name, region, and version if applicable"""
        i = self.ID
        return "{}{}{}{}".format(i.frmt, i.name, i.region, '' if not i.version else str(i.version))
    @gameID.setter
    def gameID(self, guid:str):
        if not isinstance(guid, str):  raise TypeError(f'"{guid}" must be a string!')
        if len(guid) < 4:  raise TypeError(f'"{guid}" not in (format, name, region, version) format.')
        v = int(guid[4:]) if guid[4:] else 0
        self.rom[0x3B:0x3F] = guid[0:4].encode()
        self.rom[0x3F] = v
    @gameID.deleter
    def gameID(self):
        self.rom[0x3B:0x40] = bytes(5)

    @property
    def timestamp(self):
        if any(self.rom[0x34:0x3B]):  return time.asctime(struct.unpack_from(">H5B", self.rom[0x34:0x3B])+(0, 0, 1))
        else:  return "No embedded timestamp."

    def set_timestamp(self, t=None):
        """If t is None, embeds current system time.
        If t is an int, it's treated as seconds since the whenever.
        If t is a time.struct_time object that will be encoded directly.
        If t is a str, it will (hopefully) be decoded into a time.struct_time.
        """
        if t is None or isinstance(t, int):  t = time.localtime(t)
        elif isinstance(t, str):  t = time.strptime(t)
        self.rom[0x34:0x3B] = struct.pack(">H5B", t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)

    def cmp(self, **kwargs):
        """Compares named attributes in rom ID with kwargs.
        If all attributes match kwargs, return True.
        Typically used to test if the game ID, region, or version match.
        """
        for i in kwargs.keys():
            if getattr(self.ID, i, None)!=kwargs[i]:
                return False
        return True

    @staticmethod
    def cic2seed(cic):
        """Returns (name, seed) for <cic>.
        name: the normalized name of the chip
        seed: unencoded seed value.
            <cic> should be a str, matching the serial or short name of the chip.
        cic types:
            'aleck', '5101'
            'starf', '6101'
            'lylat', '7102'
            'mario', '6102', '7101'
            'diddy', '6103', '7103'
            'ddipl', '8303'
            'dddev', '8302', '8301'
            'ddusa'
            'zelda', '6105', '7105'
            'yoshi', '6106', '7106'
            '64sdk'
        """
        # Normalize the names.  (kinda makes the dict pointless...)
        cic = cic.lower()
        if cic in ("6102", "7101", "mario"):
            cic = "mario"
        elif cic in ("6101", "starf"):
            cic = "starf"
        elif cic in ("7102", "lylat"):
            cic = "lylat"
        elif cic in ("6103", "7103", "diddy"):
            cic = "diddy"
        elif cic in ("6105", "7105", "zelda"):
            cic = "zelda"
        elif cic in ("6106", "7106", "yoshi"):
            cic = "yoshi"
        elif cic in ("5101", 'aleck'):
            cic = 'aleck'
        elif cic in ("8303", "ddipl"):
            cic = "ddipl"
        elif cic in ("8302", "8301", "dddev"):
            cic = "dddev"
        elif cic in ("ddusa"):
            cic = "ddusa"
        elif cic in ("64sdk"):
            cic = "64sdk"
        else:
            raise TypeError("Unknown CIC type {}.".format(cic))
        return cic, Cart.cic_names.get(cic)

    @property
    def crc(self):
        """Returns CRC in rom as a tuple of integers."""
        u = int.from_bytes(self.rom[16:20], byteorder='big')
        l = int.from_bytes(self.rom[20:24], byteorder='big')
        return (u, l)

    def calccrc(self, cic=None, fix=False, seed=None, base=0x1000, seel=None):
        """Recalculates the CRC based on the CIC chip version given.
        Set fix to True to revise the crc in self.rom.
        If cic is None it will be determined from the file.

        cic types:  (old scene names, followed by serial IDs)
            'aleck', '5101'
            'starf', '6101'
            'lylat', '7102'
            'mario', '6102', '7101'
            'diddy', '6103', '7103'
            'ddipl', '8303'
            'dddev'
            'ddusa'
            'zelda', '6105', '7105'
            'yoshi', '6106', '7106'
        Setting seed overrides the normal seed value for these types.
        If you need to use one of the particular cic algorithms set the
            string version of the name in cic.  Now that the Aleck64
            CIC has been identified this will probably not be necessary
            unless some repro cic chips start using different values."""
        def rol(v, n):
            return (v % 0x100000000)>>n

        # The algo slightly changes with certain cics.
        if cic is None:
            cic = self.cic
        cic, s = self.cic2seed(cic)
        # The "devcart bootstrap" doesn't run an IPL3 checksum!
        if cic == '64sdk':
            return (0,0)
        # Pull the seed byte and generate the seed from it.
        # Note even if seed is set you'll have to pass a cic "name" to get funky types out.
        if seed is not None:
            s = seed
        if cic in ('diddy', 'yoshi', 'aleck'):
            seed = 0x6C078965 * s
        elif cic in ('ddipl', 'ddusa'):
            seed = 0x2E90EDD * s
        elif cic == 'dddev':
            seed = 0x260BCD5 * s
        else:
            seed = 0x5D588B65 * s
        seed+= 1
        seed&=0xFFFFFFFF
        r1, r2, r3, r4, r5, r6 = seed, seed, seed, seed, seed, seed

        # I wish there was a less horrifying way to do this...
        if seel is None:
            if cic == 'aleck':
                seel = 0x3FE000 if self.ID.PC == 0x80100400 else 0x100000
            elif cic in ('ddipl', 'dddev', 'ddusa'):
                seel = 0xA0000
            else:
                seel = 0x100000
        v = seel + base
        l = min(v, len(self.rom))
        m = struct.unpack_from(">%dL" % (seel >> 2), self.rom[base:l] + bytes(v - l))
        # Zelda updates the second word a different way...
        if cic == 'zelda':
            from itertools import cycle
            n = struct.unpack_from(">64L", self.rom, 0x750)
            n = cycle(n)
        # Read each word as an integer.
        for i in m:
            v = (r1+i) & 0xFFFFFFFF
            if v < r1:
                if cic in ('ddipl', 'ddusa'):
                    r2^=r3
##                if cic == 'dddev':
##                    pass    # invalid opcode in v1.0: 014A0000
                else:
                    r2+=1
            v = i & 0x1F
            a = (i<<v) | (rol(i, 0x20-v))
            r1+=i
            r3^=i
            r4+=a
            # You have to limit the result here to 32bits.
            r1&= 0xFFFFFFFF
            r4&= 0xFFFFFFFF
            if r5 < i:
                r5^= (r1^i)
            else:
##                if cic in ('ddipl', 'dddev'):
                if cic  == 'ddipl':
                    r5+=a
                else:
                    r5^=a
            if cic == 'zelda':
                r6+= (i ^ next(n))
            else:
                r6+= (i ^ r4)
            # Ditto here.
            r5&= 0xFFFFFFFF
            r6&= 0xFFFFFFFF
        # Assemble upper and lower CRCs
        if cic in ('ddipl', 'dddev', 'ddusa'):
            if fix:
                if isinstance(self.rom, bytes):
                    self.rom = bytearray(self.rom)
                self.setASMvalue(r1, 0x608, 0x60C)
                self.setASMvalue(r2, 0x618, 0x61C)
                self.setASMvalue(r3, 0x628, 0x62C)
                self.setASMvalue(r4, 0x638, 0x63C)
                self.setASMvalue(r5, 0x648, 0x64C)
                self.setASMvalue(r6, 0x658, 0x65C)
            return (r1, r2, r3, r4, r5, r6)
        if cic == 'yoshi':
            r1*=r2
            r4*=r5
        else:
##            r2&= 0xFFFFFFFF
            r1^=r2
            r4^=r5
        if cic in ('diddy', 'yoshi', 'aleck'):
            r1+=r3
            r4+=r6
        else:
            r1^=r3
            r4^=r6
        # Make sure they fit within 4 bytes each.
        r1&= 0xFFFFFFFF
        r4&= 0xFFFFFFFF
        if fix:
            if isinstance(self.rom, bytes):
                self.rom = bytearray(self.rom)
            self.rom[16:20] = r1.to_bytes(4, 'big')
            self.rom[20:24] = r4.to_bytes(4, 'big')
        return (r1,r4)

    def bootstrapcrc(self, cic=None, seed=None):

        if cic is None:
            cic = self.cic
        if seed is None:
            cic, seed = self.cic2seed(cic)

        def trimult(v1, v2, v3):
            if not v2:
                v2 = v3
            l = v1 * v2
            u = (l>>32) & 0xFFFFFFFF
            l &= 0xFFFFFFFF
            u -= l
            if not u:
                u = v1
            return (u & 0xFFFFFFFF)

        def rol(v, n):
            return (v % 0x100000000)>>n

        data = struct.unpack(">1008L", self.rom[0x40:0x1000])

        seed *= 0x6C078965
        seed += 1
        seed ^= data[0]
        seed &= 0xFFFFFFFF
        regs = [seed, seed, seed, seed, seed, seed, seed, seed,
                seed, seed, seed, seed, seed, seed, seed, seed,]
        count = 0   #S1
        cur = data[0]  #S4
        # First half of the algorithm: read in IPL2 bootcode.
        while True:
            prev = cur  #S4
            cur = data[count]   #S0
            count += 1
            v = (0x3EF - count) & 0xFFFFFFFF
            regs[0] +=  trimult(v, cur, count)
            regs[0] &= 0xFFFFFFFF
            regs[1] = trimult(regs[1], cur, count)
            regs[2] ^= cur
            regs[3] += trimult(cur + 5, 0x6C078965, count)
            regs[3] &= 0xFFFFFFFF
            # BFC00250
            if prev < cur:
                regs[9] = trimult(regs[9], cur, count)
            else:
                regs[9] += cur
            regs[9] &= 0xFFFFFFFF
            # BFC00288
            b = prev & 0x1F
            u = cur << (32 - b)
            l = rol(cur, b)
            roll1 = (u | l) & 0xFFFFFFFF   #S5
            regs[4] += roll1
            regs[4] &= 0xFFFFFFFF
            b = prev & 0x1F
            u = cur << b
            l = rol(cur, 32 - b)
            v = (u | l) & 0xFFFFFFFF
            regs[7] = trimult(regs[7], v, count)
            # BFC002C0
            if cur < regs[6]:
                regs[6] += regs[3]
                regs[6] ^= cur + count
            else:
                regs[6] ^= regs[4] + cur
            regs[6] &= 0xFFFFFFFF
            # BFC002FC
            b = prev >> 0x1B
            u = cur << b
            l = rol(cur, 32 - b)
            roll2 = (u | l) & 0xFFFFFFFF   #S2
            regs[5] += roll2
            regs[5] &= 0xFFFFFFFF
            u = cur << (32 - b)
            l = rol(cur, b)
            v = (u | l) & 0xFFFFFFFF
            regs[8] = trimult(regs[8], v, count)
            if count == 0x3F0:
                break
            future = data[count]    #S3
            # BFC00340
            regs[15] = trimult(regs[15], roll2, count)
            b = rol(cur, 0x1B)
            u = future << b
            l = rol(future, 32 - b)
            v = (u | l) & 0xFFFFFFFF
            regs[15] = trimult(regs[15], v, count)
            # BFC00374
            regs[14] = trimult(regs[14], roll1, count)
            b = cur & 0x1F  #S2
            u = future << (32 - b)
            l = rol(future, b)
            v = (u | l) & 0xFFFFFFFF
            regs[14] = trimult(regs[14], v, count)
            # BFC003A4
            u = cur << (32 - b)
            l = rol(cur, b)
            v = (u | l) & 0xFFFFFFFF
            regs[13] += v
            b = future & 0x1F
            u = future << (32 - b)
            l = rol(future, b)
            v = (u | l) & 0xFFFFFFFF
            regs[13] += v
            regs[13] &= 0xFFFFFFFF
            # BFC003DC
            regs[10] += cur
            regs[10] = trimult(regs[10] & 0xFFFFFFFF, future, count)
            regs[11] = trimult(regs[11] ^ cur, future, count)
            regs[12] += regs[8] ^ cur
            regs[12] &= 0xFFFFFFFF
        # BFC00420
        buf = [regs[0], regs[0], regs[0], regs[0]]
        for i in range(16):
            cur = regs[i]
            b = cur & 0x1F
            u = cur << (32 - b)
            l = rol(cur, b)
            v = (u | l) & 0xFFFFFFFF
            buf[0] += v
            buf[0] &= 0xFFFFFFFF
            if cur < buf[0]:
                buf[1] += cur
                buf[1] &= 0xFFFFFFFF
            else:
                buf[1] = trimult(buf[1], cur, i)
            # BFC00494
            if (cur & 3) in (0, 3):
                buf[2] += cur
                buf[2] &= 0xFFFFFFFF
            else:
                buf[2] = trimult(buf[2], cur, i)
            # BFC004CC
            if cur & 1:
                buf[3] ^= cur
            else:
                buf[3] = trimult(buf[3], cur, i)
        # BFC00504
        v = trimult(buf[0], buf[1], 16)
        b = buf[2] ^ buf[3]
        return (v & 0xFFFFFFFF, b & 0xFFFFFFFF)

    @property
    def byteorder(self):
        # Lazy test for byteswapping
        if self.rom[0]==128:
            return 'big'
        elif self.rom[1]==128:
            return 'little'
        else:
            raise ValueError("Unable to determine byteorder from initial PI settings.")

    def straighten(self):
        """Sets self.rom big endian, byteswapping if necessary."""
        if self.byteorder == 'little':
            self.rom = byteswap(self.rom)

    def getASMvalue(self, upper, lower=None):
        """If <upper> and <lower> are given,
            reads the address given by a LUI+ADDIU or LUI+ORI pair.
            If upper is None only the lower half will be read.
        If only <upper> is given,
            reads the address in a J or JAL instruction, | 80000000."""
        if lower is None:
            u = int.from_bytes(self.rom[upper+2:upper+4], 'big') & 0x3FFFFFF
            u<<=2
            return u | 0x80000000
        if upper is None:
            u = 0
        else:
            u = int.from_bytes(self.rom[upper+2:upper+4], 'big')<<16
        e = False if self.rom[lower]&0x10 else True
        l = int.from_bytes(self.rom[lower+2:lower+4], 'big', signed=e)
        return u+l

    def setASMvalue(self, value, upper, lower=None):
        """If <upper> and <lower> are given,
            sets the LUI+ADDIU or LUI+ORI pair to value.
            If upper is None only the lower half will be set.
        If only <upper> is given,
            sets value as the address of a J or JAL instruction."""
        if lower is None:
            value>>=2
            value&=0x3FFFFFF
            u = self.rom[upper] & 0xFC
            value|= u<<24
            self.rom[upper:upper+4] = value.to_bytes(4, 'big')
        else:
            u = value>>16
            l = value&0xFFFF
            if not self.rom[lower]&0x10:
                u += bool(l&0x8000)
            if upper is not None:
                self.rom[upper+2:upper+4] = u.to_bytes(2, 'big')
            self.rom[lower+2:lower+4] = l.to_bytes(2, 'big')

# This is not generic at all.
def tickit(cmd, rom, contentID=0x3E9BC0, **kwargs):
    from hashlib import sha1
    q = bytearray(cmd)
    q[0:0x2800] = bytes(0x2800)
    # Unless there's accessible hidden ram, the latest address this can be located at is 0x807FC000.  On hardware it is always one block (0x4000)
    # Set to 0x800 for 16k, it's now supported.
    q[0:0x38] = struct.pack(">14L", kwargs.get('eeprom', 0), kwargs.get('eepsz', 0),
                                 kwargs.get('flash', 0), kwargs.get('flasz', 0),
                                 kwargs.get('sram', 0), kwargs.get('srasz', 0),
                                 kwargs.get('pak0', 0), kwargs.get('pak1', 0), kwargs.get('pak2', 0), kwargs.get('pak3', 0),
                                 kwargs.get('paksz', 0), kwargs.get('osRomBase', 0xB0000000), kwargs.get('osTvType', 1), kwargs.get('osMemSize', 0x800000))
    q[0x40:0x43] = b'CAM'
    q[0x43] = kwargs.get('u0x_cnt', 0)
    q[0x44:0x44+len(aegh)] = aegh
    q[0x280C:0x2810] = UL.pack(len(rom))
    q[0x2824:0x2838] = sha1(rom).digest()
    q[0x2848:0x2850] = struct.pack(">LL", 2, kwargs.get('execFlags', 0x20))  # execFlags & access flags; 0x20 required for 8MB games!
    q[0x2898:0x289C] = UL.pack(contentID) # contentID
    if len(q) > 0x29AC:
        q[0x29B0:0x29B6] = struct.pack(">3H", 0x80, 0, 0) # ticketID, trial type + limit
    return bytes(q)


from tkinter import Tk, filedialog, messagebox, Menu, IntVar, StringVar, Toplevel, Pack, Grid
from tkinter.ttk import Button, Checkbutton, Frame, Label, Entry, Scrollbar, Labelframe, OptionMenu, Radiobutton, Spinbox
from tkinter.constants import *
from idlelib.tooltip import Hovertip

titlebar = "Q-Vert: Mother's Day 2023"
def_exts = (("All Files","*"),('Binary File', '*.bin'),)
txt_exts = (('Text File', '*.txt'),("All Files","*"),)
tik_exts = (('iQue Ticket', '*.dat'),)
sys_exts = (('iQue Ticket Collection', '*.sys'),)
app_exts = (('iQue Application', '*.app'),)
rom_exts = (("N64 File","*.n64"),("N64 File","*.z64"),("N64 File","*.rom"),)

class Application(Frame):
    def close_file(self, Event=None, index=0):
        self.input_data = None
        self._magikarp()

    def open_file(self, Event=None, index=0):
        fname = filedialog.askopenfilename(title="Open ROM", filetypes=rom_exts + def_exts)
        if not fname or not os.path.exists(fname):
            return
        try:
            with open(fname, 'rb') as f:
                cart = Cart(f.read())
            # Might as well do the thing here...
            symbols = dict()
            lst = [i[0] for i in map(os.path.splitext, os.listdir('GE')) if i[-1].lower() == '.py']
            if not self.noise_lvl.get():  print('\n\t'.join((f'Detected modules:', *lst)))
            if not self.noise_lvl.get():  print('\nDetected function addresses:')
            for m in lst:	# Gather all symbols, then do the patches.
                if not importlib.util.find_spec(m):
                    raise RuntimeError(f'"{m}.py" not found.')
                k = importlib.import_module(m)
                pos = search_all(cart.rom, k.sample, getattr(k, 's_mask', None), 0x1000, 4, None)
                if not pos:
                    if getattr(k, 'optional', False):
                        if not self.noise_lvl.get():  print(f'\t*Optional module "{m}" not found.  Continuing...')
                        continue
                    raise RuntimeError(f'Sample in "{m}.py" not found.')
                if not self.noise_lvl.get():  print(f'\t{m}: ' + ', '.join(f'0x{i:X}' for i in pos))
                if hasattr(k, 'symbol'):	# Hackish & lousy, replace later.
                    p = pos[0]	# BUG: you will lose symbols f/ any duplicates, esp. serial device handlers!
                    for j in k.symbol:	# TODO: Spin off as a small something elsewhere.
                        v = j[0].lower()
                        if v == '+':	# Add j[2] to sample's location (for subfunctions, branch/jump targets, peculiar/alternate entrypoints, injected functions, etc.)
                            symbols[j[1]] = j[2] + p
                        elif v in 'jl':	# Read address from a J/JAL instruction.
                            symbols[j[1]] = ((UL.unpack_from(cart.rom, p+j[2])[0] & 0x3FFFFFF) << 2) + j[3]
                        elif v == 'a':	# Upper/lower address pair f/ ASM.
                            v = UH.unpack_from(cart.rom, p+j[2])[0] << 16
                            v |= UH.unpack_from(cart.rom, p+j[3])[0]
                            if (cart.rom[p+j[3]-2] >> 2) in (8, 9, 0x18, 0x19) and (v & 0x8000): v -= 0x10000
                            symbols[j[1]] = v
                        elif v == '@':	# Literal/static value assigned to symbol.
                            symbols[j[1]] = j[2]
                        elif v == 'f':	# A dirty hack for dirty hacks.  ('f', int, symbol, value)
                            v, pp = j[1], p
                            while v:
                                if cart.rom[pp] & 0xFC in (8, 0xC):  v -= 1
                                pp += 4
                            symbols[j[2]] = ((UL.unpack_from(cart.rom, pp-4)[0] & 0x3FFFFFF) << 2) + j[3]
                if hasattr(k, 'ex_sym'):	# Extended symbols, even worse than normal ones.
                    for e in k.ex_sym:
                        p = symbols[e[0]] + e[1]	# This should correct it to a ROM address.
                        for j in e[2]:
                            v = j[0].lower()
                            if v == '+':	# Add j[2] to sample's location (for subfunctions, branch/jump targets, peculiar/alternate entrypoints, injected functions, etc.)
                                symbols[j[1]] = j[2] + p
                            elif v in 'jl':	# Read address from a J/JAL instruction.
                                symbols[j[1]] = ((int.from_bytes(cart.rom[p+j[2]:p+j[2]+4], 'big') & 0x3FFFFFF) << 2) + j[3]
                            elif v == 'a':	# Upper/lower address pair f/ ASM.
                                v = int.from_bytes(cart.rom[p+j[2]:p+j[2]+2], 'big') << 16
                                v |= int.from_bytes(cart.rom[p+j[3]:p+j[3]+2], 'big')
                                if (cart.rom[j[3]-2] >> 2) in (8, 9, 0x18, 0x19) and (v & 0x8000): v -= 0x10000
                                symbols[j[1]] = v
                            elif v == '@':	# Literal/static value assigned to symbol.
                                symbols[j[1]] = j[2]
                            elif v == 'f':	# A dirty hack for dirty hacks.
                                v, pp = j[1], p
                                while v:
                                    if cart.rom[pp] & 0xFC in (8, 0xC):  v -= 1
                                    pp += 4
                                symbols[j[2]] = ((int.from_bytes(cart.rom[pp-4:pp], 'big') & 0x3FFFFFF) << 2) + j[3]
            if not self.noise_lvl.get():  print('\n\t'.join(('\nRegistered symbols:', *(f'{symbols[i]:X}: "{i}"' for i in symbols))))
            for m in lst:	# Patches.  On second thought should be their own thing, not integrated in sample specs...  Maybe just recycle the other game patch doodly?
                # Already threw all the errors you're going to throw.
                k = importlib.import_module(m)
                if hasattr(k, 'p_condition'):
                    if not getattr(self, k.p_condition).get():  continue
                if hasattr(k, 'p_data') or hasattr(k, 'p_cmds'):
                    if not self.noise_lvl.get():  print(f'Patching {m}')
                    pos = search_all(cart.rom, k.sample, getattr(k, 's_mask', None), 0x1000, 4, None)
                    if not pos:
                        if not getattr(k, 'optional', False):  continue
                        raise RuntimeError(f'Sample in "{m}.py" not found.')
                    sz = len(k.sample)
                    for j in pos:
                       cart.rom[j:j+sz] = patch(cart.rom[j:j+sz], k, **symbols)
            # Re-header for non-iQue.
            cart.rom[0x3B] = 0x42	# Set type as 'B', as in BB Player
            if not self.noise_lvl.get():   print(f'Finalizing "{cart.gameID}"...')
            # Pad & truncate in an obviously flawed way.
            v = bytes(0xFF for i in range(0x4000))
            cart.rom.extend(v[len(cart.rom) & 0x3FFF:])
            while cart.rom[-0x4000:] == v:  del cart.rom[-0x4000:]
            v = bytes(0x4000)
            cart.rom.extend(v[len(cart.rom) & 0x3FFF:])
            while cart.rom[-0x4000:] == v:  del cart.rom[-0x4000:]
            cart.calccrc('6102', fix=True)
            cart.set_timestamp()
            self.input_data = bytes(cart.rom)
            self.fname_cart.set(f"Loaded ROM: {os.path.basename(fname)}")
            print(f'"{fname}" loaded successfully!\n({"Slow" if self.anim_rate.get() else "Normal"} animation rate applied.  Reload ROM to change this setting.)')
        except:
            print(sys.exc_info()[1])
            print(f'Failed loading "{fname}".')
        # Retains old copy in case of failure.
        # self.out_fileEnable.set(bool(self.input_data))
        self._magikarp()

    def open_ticket(self, Event=None, index=0):
        fname = filedialog.askopenfilename(title=f"Open Ticket", filetypes=tik_exts + def_exts)
        if not fname or not os.path.exists(fname):
            return
        with open(fname, 'rb') as f:
            o = f.read()
            # There's probably a bunch of undumb tests to run here.
            if len(o) != 0x2B4C:
                print("Standalone tickets should be 0x2B4C in size.")
            elif o[0x2858:0x2860] != b'Root-CPC':
                print("Contents seem iffy.  Try another file?")
            else:
                self.fname_cmd.set(f"Loaded CMD: {os.path.basename(fname)}")
                self.cmd = o
                self.sys = None
                print(f'"{fname}" loaded successfully!')
        # Retains old copy in case of failure.
        self._magikarp()

    def open_ticketsys(self, Event=None, index=0):
        fname = filedialog.askopenfilename(title=f"Open Ticket Collection", initialfile="hackit.sys", filetypes=sys_exts + def_exts)
        if not fname or not os.path.exists(fname):
            return
        with open(fname, 'rb') as f:
            o = bytearray(f.read())
            # There's probably a bunch of undumb tests to run here.
            v = UL.unpack_from(o)[0]
            if v not in range(1, 0x800000):
                print("File not usable.")
            elif o[0x285C:0x2864] != b'Root-CPC':
                print("Contents seem iffy.  Try another file?")
            else:
                del o[(v * 0x2B4C) + 4:]
                self.fname_cmd.set(f"Loaded SYS: {os.path.basename(fname)}")
                self.sys = o
                self.cmd = None
                print(f'"{fname}" loaded successfully!')
        # Retains old copy in case of failure.
        self._magikarp()


    def write_file(self, Event=None):
        if not self.out_fileEnable.get():
            return
        i = filedialog.asksaveasfilename(title="Save File As:", filetypes=rom_exts + def_exts, defaultextension='.n64')
        if i:
            with open(i, 'wb') as f:
                f.write(self.input_data)
                print(f'ROM written to "{i}".')
        
        self.update_idletasks()
    
    def write_iQue(self, Event=None):
        if not self.out_ticketEnable.get() or not self.out_fileEnable.get():
            return
        contentID = self.contentID.get()
        if not isinstance(contentID, int):
            print("*Warning: please set something resembling a valid contentID.")
            return
        # Validate the EEPROM target address.
        eepsz = self.EEP16k.get()
        v = (eepsz + 0x3FFF) & ~0x3FFF
        i = ''.join(i for i in self.EEPROM.get() if i in '0123456789abcdefABCDEF')
        if not i:
            print(f'*Warning: Invalid EEPROM address.\n\tValue must be between 80000400 and {0x90000000-v:X}. (Not that those are all "valid"...)')
            return
        eep = int(i, 16)
        if eep not in range(0x80000400, 0x90000000-v):
            print(f'*Warning: Invalid EEPROM address.\n\tValue must be between 80000400 and {0x90000000-v:X}. (Not that those are all "valid"...)')
            return

        i = filedialog.askdirectory(title="Output Directory:", mustexist=True)
        if i and os.path.exists(i):
            if self.sys:
                v = UL.unpack_from(self.sys)[0]
                del self.sys[4 + (v * 0x2B4C):] # truncate!
                for j in range(v):
                    if UL.unpack_from(self.sys, 0x289C + (j * 0x2B4C))[0] == contentID:
                        if not self.noise_lvl.get(): print(f'Existing ticket found in entry {j+1}.')
                        self.cmd = tickit(self.sys[(j * 0x2B4C)+4:(j * 0x2B4C)+0x2B50], self.input_data, contentID, eeprom=eep, eepsz=eepsz)
                        self.sys[(j * 0x2B4C)+4:(j * 0x2B4C)+0x2B50] = self.cmd
                        break
                else:
                    if not self.noise_lvl.get(): print(f'Inserting new ticket for {contentID}.')
                    for j in range(v):	# Find first valid one.
                        if bytes(self.sys[(j * 0x2B4C)+0x2818:(j * 0x2B4C)+0x2828]) == b'V\x82!\xeewa-\x986\xe9+u\xa6\x96.\x7f':  break
                    else:	# This *should* be an impossible condition...
                        print("*Error: Unable to find ticket for 马力欧医生 (Dr.Mario 64), suspect this isn't a valid file.\nAborting...")
                        return
                    self.cmd = tickit(self.sys[(j * 0x2B4C)+4:(j * 0x2B4C)+0x2B50], self.input_data, contentID, eeprom=eep, eepsz=eepsz)
                    self.sys[0:4] = UL.pack(v + 1)
                    self.sys[4:4] = self.cmd
                # Pad back to correct size.
                v = 0x4000 - len(self.sys)
                k = self.sys + bytes(v & 0x3FFF)
                j = os.path.join(i, f'hackit.sys')
                with open(j, 'wb') as f:
                    f.write(k)
                    print(f'Updated ticket collection written to "{j}".')
                j = os.path.join(i, f'{contentID:08x}.dat')
                with open(j, 'wb') as f:
                    f.write(self.cmd)
                    print(f'Ticket written to "{j}".')
            else:
                if not self.noise_lvl.get(): print(f'Generating ticket "{contentID:08x}.dat".')
                v = tickit(self.cmd, self.input_data, contentID, eeprom=eep, eepsz=eepsz)
                j = os.path.join(i, f'{contentID:08x}.dat')
                with open(j, 'wb') as f:
                    f.write(v)
                    print(f'Ticket written to "{j}".')
            if self.encodeOK.get():
                if not self.noise_lvl.get(): print(f'Encrypting "{contentID:08x}.app".\nPour some tea and grab a book, this may take a while.')
                v = encodeApp(self.cmd, self.input_data)
                j = os.path.join(i, f'{contentID:08x}.app')
                with open(j, 'wb') as f:
                    f.write(v)
                    print(f'Application written to "{j}".')
        
        self.update_idletasks()
    
    def createWidgets(self):
        """Place widgets here."""
        #Allow resizing
        top = self.winfo_toplevel()
        top.minsize(80,80)
        top.resizable(False, False)
        top.rowconfigure(0, weight=1)
        top.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.columnconfigure(1, weight=1)

        self.updateTbl = []
        e = bool(encrypt)

        tips = {'header':"Provide some version of GoldenEye that isn't PAL.\nAccepts big or little endian input files.\nOutput ROM is always big endian.",
                'ticket':"Provide a stand-alone ticket from your iQue.\nA new ticket will be generated from it to avoid ECDH nonsense.",
                'tiksys':"Updates a ticket.sys file, basing a new ticket off an existing one.\nIf a ticket already exists, it will be updated.",
                'encode':"If enabled, outputs a once-encoded iQue .app along with ticket.\nFair warning, may take a while to encrypt.\nOnly available if the pyCryptodome package is installed or 'aes.py' is in the same directory.",
                'eep4k':"GoldenEye usually uses 4k EEPROM.\nIrregardless the setting, both sizes are supported.",
                'eep16k':"16k EEPROM is implemented if called for.\nIrregardless the setting, both sizes are supported.",
                'noisy':"Enables and disables debug output in the other window.\n(Doesn't close it and doesn't make it closable though.)",
                'content':"The iQue contentID.  Default is 4103104.",
                'eeprom':'Runtime location of iQue EEPROM buffer.\nIn "normal" RDRAM, highest address can be 807FC000.\nDefault is 806FFE00, safe for vanilla & "7MB" mods.\n806D0000 is recommended for 640i mod.',
                'anim_norm':"* Only applied when loading a ROM! *\nUses the normal playback rate of 100% on iQue.\nGoldenEye doesn't throttle framerate in any practical\nway and to many players this feels too fast.\nN64-alikes running this ROM are unaffected.",
                'anim_slow':"* Only applied when loading a ROM! *\nAdjusts iQue's animation rate to simulate N64 speed.\nN64-alikes running this ROM are unaffected.\nNote replays are unaffected to prevent breaking playback.\nAs a result, if you were to record one via iQue it will fail.\n* Right now it doesn't affect animated objects.\n  Certain cutscenes will look wrong.",
                }

        # Top: Instruct, net pswd + button.
        in_files = Labelframe(self, text="Open Files")
        in_files.grid(row=1, column=0, sticky='NWE')
        i = Button(in_files, text="Load N64 ROM", command=self.open_file)
        i.grid(row=0, column=0, sticky='NWE')
        Hovertip(i, tips[f'header'], hover_delay=1000)
        i = Button(in_files, text="Load Ticket", command=self.open_ticket)
        i.grid(row=1, column=0, sticky='NWE')
        Hovertip(i, tips[f'ticket'], hover_delay=1000)
        i = Button(in_files, text="Load Ticket Collection", command=self.open_ticketsys)
        i.grid(row=2, column=0, sticky='NWE')
        Hovertip(i, tips[f'tiksys'], hover_delay=1000)

        output = Labelframe(self, text="Output")
        output.grid(row=1, column=1, sticky='NWE')
        out_file = Button(output, text="Save N64 ROM", state=DISABLED, command=self.write_file)
        out_file.grid(row=0, column=0, sticky='NWE')
        self.updateTbl.append((out_file, self.out_fileEnable))
        out_tik = Button(output, text=f"Save Ticket{' + App' if e else ''}", state=DISABLED, command=self.write_iQue)
        out_tik.grid(row=1, column=0, sticky='NWE')
        self.updateTbl.append((out_tik, self.out_ticketEnable))

        self.encodeOK.set(e)	# Should only be available if a function to encode already exists.
        opts = Labelframe(self, text="Options")
        opts.grid(row=2, column=0, sticky='NWE')
        i = Checkbutton(opts, text="Quiet", variable=self.noise_lvl, command=self._magikarp)
        i.grid(row=0, column=0, sticky='NW')
        Hovertip(i, tips[f'noisy'], hover_delay=1000)
        i = Checkbutton(opts, text="Encrypt", variable=self.encodeOK, state=NORMAL if e else DISABLED, command=self._magikarp)
        i.grid(row=1, column=0, sticky='NW')
        Hovertip(i, tips[f'encode'], hover_delay=1000)
        i = Radiobutton(opts, text="4k EEPROM", variable=self.EEP16k, value=0x200, command=self._magikarp)
        i.grid(row=2, column=0, sticky='NW')
        Hovertip(i, tips[f'eep4k'], hover_delay=1000)
        i = Radiobutton(opts, text="16k EEPROM", variable=self.EEP16k, value=0x800, command=self._magikarp)
        i.grid(row=3, column=0, sticky='NW')
        Hovertip(i, tips[f'eep16k'], hover_delay=1000)

        settings = Labelframe(self, text="iQue Settings")
        settings.grid(row=2, column=1, sticky='NWE')
        user_l = Label(settings, text="ContentID")
        user_l.grid(row=0, column=0, columnspan=1, sticky='W')
        user_v = Spinbox(settings, justify=RIGHT, width=11, textvariable=self.contentID, from_=0, to=4294967295, wrap=False, command=self._magikarp)
        user_v.grid(row=0, column=1, columnspan=1, sticky='W')
        Hovertip(user_v, tips[f'content'], hover_delay=1000)
        tour_l = Label(settings, text="EEPROM Addr.")
        tour_l.grid(row=1, column=0, columnspan=1, sticky='W')
        tour_v = Entry(settings, justify=RIGHT, width=11, textvariable=self.EEPROM)
        tour_v.grid(row=1, column=1, columnspan=1, sticky='W')
        Hovertip(tour_v, tips[f'eeprom'], hover_delay=1000)

        i = Radiobutton(settings, text="Normal Speed  (at ROM load)", variable=self.anim_rate, value=False, command=self._magikarp)
        i.grid(row=2, column=0, columnspan=2, sticky='NW')
        Hovertip(i, tips[f'anim_norm'], hover_delay=1000)
        i = Radiobutton(settings, text="Speed -18.75%  (at ROM load)", variable=self.anim_rate, value=True, command=self._magikarp)
        i.grid(row=3, column=0, columnspan=2, sticky='NW')
        Hovertip(i, tips[f'anim_slow'], hover_delay=1000)

        #base.grid(row=3, column=0, columnspan=2, sticky='SW')
        rom_v = Label(self, textvariable=self.fname_cart)
        rom_v.grid(row=3, column=0, columnspan=2, sticky='W')
        self.updateTbl.append((rom_v, self.fname_cart))
        cmd_v = Label(self, textvariable=self.fname_cmd)
        cmd_v.grid(row=4, column=0, columnspan=2, sticky='W')
        self.updateTbl.append((cmd_v, self.fname_cmd))


    def _magikarp(self, Event=None):
        self.out_fileEnable.set(bool(self.input_data))
        self.out_ticketEnable.set(self.out_fileEnable.get() & (bool(self.cmd) | bool(self.sys)))
        # Insert anything that needs to be redrawn here.
        for i in self.updateTbl:
            j = NORMAL if len(i) < 3 else i[2]
            k = DISABLED if len(i) < 4 else i[3]
            i[0]["state"]=j if i[1].get() else k

        self.update_idletasks()

    def _reinit(self, Event=None):
        self.out_fileEnable.set(False)
        self.out_ticketEnable.set(False)
        self.input_data = None
        self.cmd = None
        self.noise_lvl.set(False)
        self.contentID.set(0x3e9bc0)
        self.EEPROM.set("806FFE00")
        self.EEP16k.set(0x200)
        self.anim_rate.set(False)
        self.fname_cart.set('Loaded ROM: None')
        self.fname_cmd.set('Loaded CMD: None')

    def _reset(self, Event=None):
        self._reinit()
        self._magikarp()

    def __init__(self, master=None):
        print(f"{titlebar}\n  Don't close this window!\n  Messages are printed here!\n")
        # TK initialization
        Frame.__init__(self, master)
        self.master.title(titlebar)
        self.grid_configure(padx=3, pady=3, ipadx=3, ipady=3, sticky=NSEW)
        # General variables
        self.input_data = None
        self.cmd = None
        self.sys = None
        # TK variables
        self.out_fileEnable = IntVar()
        self.out_ticketEnable = IntVar()
        self.encodeOK = IntVar()
        self.noise_lvl = IntVar()
        self.contentID = IntVar()
        self.EEPROM = StringVar()
        self.EEP16k = IntVar()
        self.anim_rate = IntVar()
        self.fname_cart = StringVar()
        self.fname_cmd = StringVar()
        # build window, menubar, and map hotkeys
        self._reinit()
        self.createWidgets()
        ##self.createMenubar()
        ##self.accelerators()
        # Update anything that needs to be redrawn.
        self._magikarp()

def UI():
    # Initialize the UI and Go!
    root = Tk()
    app = Application(master=root)

    app.mainloop()

    try:    root.destroy()
    except: pass


def main():
    UI()

if __name__ == '__main__':
    main()
